/**
Copyright Christiaan Ottow 2009
chris@6core.net

See README for info
 
 This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package visim;

import java.net.*;
import java.util.*;
import java.io.*;
import java.util.regex.*;

public class Client extends Thread
{

	private int id;
	private int counter = 0;
	private long sleep_time = 100;
	private Vector<Integer> results;
	private boolean stop = false;
	private String host;
	private int port;
	private InetAddress localAddr = null;
	private DataInputStream in;
	private DataOutputStream out;
	private Socket sock;
	private Vector<String> getRequests;
	private TreeMap<String,String> postRequests;
	private File logfile;
	private PrintWriter logWriter;
	private String logname;
	private int requests = 0;
	private boolean logResponses = false;
	private String logDir = ".";
	
	public Client( int id, Vector<String> get, TreeMap<String,String> post )
	{
		this.id = id;
		this.getRequests = get;
		this.postRequests = post;
		this.results = new Vector<Integer>();
	}
	
	public void setLogDir( String dir )
	{
		this.logDir = dir;
	}
	
	public void setHost( String host )
	{
		this.host = host;
	} 
	
	public void setPort( int port )
	{
		this.port = port;
	}

	public void setSleepTime( long sleep_time )
	{
		this.sleep_time = sleep_time;
	}
	
	public void setLogfile( String logfile )
	{
		this.logname = logfile;
	}
	
	public void setResponseLogging( boolean do_log )
	{
		this.logResponses = do_log;
	}
	
	public void setLocalAddr( String localAddr )
	{
		if( localAddr != null )
		{
			try {
				this.localAddr = InetAddress.getByName( localAddr );
			} catch( Exception e ) {
				System.err.println( "Caught "+e.getClass()+" in Client" );
			}
		}
	}
	
	public int getClientId()
	{
		return this.id;
	}
	
	public void stopNext()
	{
		this.stop = true;
	}
	
	private void logWrite( String text )
	{
		try {
			logWriter.println( "Thread "+id+": "+text );
			logWriter.flush();
		} catch( Exception e ) {
			System.err.println(e.getClass()+" in thread "+getClientId()+", quitting");
			System.exit(1);
		}
	}
	
	public void run()
	{
		// open log file
		try {
			this.logfile = new File( this.logname );
			this.logWriter = new PrintWriter( logfile );
		} catch( Exception e ) {
			System.err.println("Exception "+e.getClass()+" in thread "+getClientId()+" while opening logfile "+logname );
			System.exit(1);
		}
		
		logWrite( "Using local address "+localAddr );
		int init_sleep = (int)(Math.random() * sleep_time );
		logWrite("initial sleep is "+init_sleep);
		
		doSleep( init_sleep );
		while( !stop )
		{
			for( String path : getRequests )
			{
				logWrite( "Request: GET "+path );
				request( new GetRequest( host, path ) );
			}
			for( Map.Entry<String,String> req : postRequests.entrySet() )
			{
				logWrite( "Request: POST "+req.getKey()+", "+req.getValue() );
				request( new PostRequest( host, req.getKey(), req.getValue() ) );
			}
			doSleep( this.sleep_time );
		}
		
		// close logfile
		logWriter.close();
	}
	
	private void doSleep( long length )
	{
		try {
			Thread.sleep( length );
		} catch( Exception e ) {
			System.err.println( "Caught exception" );
		}
		counter++;
	}
	
	public Vector<Integer> getResults()
	{
		return results;
	}
	
	private void request( HttpRequest request )
	{
		Vector<String> headers = new Vector<String>();
		requests++;
		
		logWrite( "Sending request" );
		try {

			// create and connect socket
			if( localAddr != null )
			{
				this.sock = new Socket( host, port, localAddr, 0 );
			} else {
				this.sock = new Socket( host, port );
			}
			
			in = new DataInputStream( sock.getInputStream() );
			out = new DataOutputStream( sock.getOutputStream() );
			
			request.send( out );
			
			logWrite("Request sent");
			
			String ret = "empty";
			int length = -1;
			
			Pattern contentLength = Pattern.compile( "Content-Length:\\s?(\\d+)", Pattern.CASE_INSENSITIVE );
			Pattern responseCode = Pattern.compile( ".*(\\d{3}).*" );
			Matcher m;
			int code = 0;

			File outfile = null;
			FileOutputStream fw = null;
			if( logResponses )
			{
				outfile = new File( logDir+"/"+getClientId()+"_output_request"+requests);
				fw = new FileOutputStream( outfile );
			}

			// time stamp
			Calendar cal = Calendar.getInstance();
			long start_stamp = cal.getTimeInMillis();

			while ( ret != null && !ret.equals("")) 
			{
				ret = this.readLine();
				headers.add( ret );
				if( code == 0 )
				{
					m = responseCode.matcher( ret );
					if( !m.matches() )
					{
						System.err.println( "Thread "+id+": Could not find response code" );
						sock.close();
						System.exit(1);
						return;
					}
					code = Integer.parseInt( m.group( 1 ) );
					if( code != 200 )
					{
						System.err.println( "Thread "+id+": Received wrong response code ("+code+")" );
						results.add( new Integer( -1 ) );
						return;
					}
				}
				
				m = contentLength.matcher( ret );
				if( m.matches() )
				{
					length = Integer.parseInt( m.group(1) );
				}
			}
			
			if( length == 0 )
			{
				System.err.println( "Warning: thread "+getClientId()+": received content-length of 0, can't be good");
				results.add( -1 );
				return;
			}

			byte[] b = new byte[1024];
			int bytesRead = 0;
			int readNow = 0;
			
			if( length > -1 )			// using Content-Length header to determine number of bytes to read
			{
				while( bytesRead < length )
				{
					readNow = in.read( b);
					if( logResponses )
					{
						fw.write( b, 0, readNow );
					}
					bytesRead += readNow;
				}
			} else {					// no Content-Length header; assuming Transfer-Encoding: chunked
				int chunksize = 0;
				boolean sizeRead = false;
				boolean haveChunk = true;
				int readOffset = 0;
				int chunksize_len = 0;
				
				while( haveChunk && readNow != -1 )
				{
					readNow = in.read( b);

					if( !sizeRead ) // no chunksize received yet
					{
						// read first line, it is the chunk size
						int start, end;
						for( start=0; start<readNow; start++ )
						{
							if( b[start] != '\n' && b[start] != '\r' )
							{
								break;
							}
						}
						
						for( end=start; end<readNow; end++ )
						{
							if( b[end] == '\n' || b[end] == '\r' )
							{
								break;
							}
						}
				
						if( start == end )
						{
							haveChunk = false;
						} else {
						
							String s = new String( b, start, end );
							readOffset = end;
							try {
								chunksize = Integer.valueOf(s, 16).intValue();
							} catch( Exception e ) {
								chunksize = readNow;
								readOffset = 0;
							}
							sizeRead = true;
							if( chunksize == 0 )
							{
								haveChunk = false;
								continue;
							}
						}
					}

					if( haveChunk )
					{
					
						if( logResponses )
						{
							fw.write( b, readOffset, ( readNow - readOffset ));
						}
						bytesRead += readNow;
						
						if( bytesRead >= chunksize )
						{
							chunksize = 0;
							sizeRead = false;
							bytesRead = 0;
						}
						
						if( b[readNow-1] == '\n' && b[readNow-2] == '\r' )
						{
							chunksize = 0;
							sizeRead = false;
							bytesRead = 0;
						}

					}
	
					readOffset = 0;
				}
			}
			
			if( logResponses )
			{
				fw.close();
			}

			// timestamp		
			cal = Calendar.getInstance();
			long end_stamp = cal.getTimeInMillis();
			int diff = (int)(end_stamp - start_stamp);
			
			// results
			logWrite("Response of "+bytesRead+" bytes received in "+diff+" msecs");
			results.add( new Integer(diff) );

			// close socket
			this.sock.close();

		} catch( Exception e ) {
			System.out.println( "Thread "+id+": Exception caught: "+e.getClass()+": "+e.getMessage() );
			e.printStackTrace();
		}
	}
	
	private String readLine() throws IOException
	{
		byte[] line = new byte[1000];
		int i=0,c;
		boolean got_r = false;
		boolean line_end = false;
		
		while( !line_end )
		{
			c = in.read();
			if( c =='\r' )
			{
				got_r = true;
			} else if( ( got_r && c == '\n' ) || i >= 1000 ) {
				line_end = true;
			} else {
				line[i] = (byte)c;
				i++;
			}
		}
		
		return new String( line, 0, i );
	}
	
}